/*-----------------------------------------------------------------------------
 * file:  sr_icmp.h 
 * date:  Thu Oct 18, 2012  
 * Author: yujiali@cs.toronto.edu
 *
 * Description:
 *
 * Methods and datastructures for handeling the ICMP messages
 *---------------------------------------------------------------------------*/

#include <assert.h>
#include <sys/types.h>
#include <string.h>
#include <pthread.h>
#include <time.h>

#include "sr_router.h"
#include "sr_icmp.h"
#include "sr_ip.h"
#include "sr_protocol.h"

uint8_t icmp_pkt_buf[IP_MAXPACKET + sizeof(struct sr_ethernet_hdr)];

pthread_mutex_t icmp_buf_mutex = PTHREAD_MUTEX_INITIALIZER;

extern pthread_mutex_t arp_q_mutex;

/* only for debugging */
void sr_print_icmp_hdr(struct sr_icmp_hdr*);

/*--------------------------------------------------------------------- 
 * Method: sr_handle_icmp(struct sr_instance* sr,uint8_t* packet, 
 *      unsigned int len)
 *
 * Scope: global
 *
 * This method is called every time an ICMP request is received. The packet
 * buffer is complete with IP headers and ICMP packet is inside it (no 
 * Ethernet headers).
 *
 * After processing the ICMP packet, a 0 is returned if everything goes
 * well, otherwise an error code is returned.
 *
 *---------------------------------------------------------------------*/

int sr_handle_icmp(struct sr_instance* sr, 
        uint8_t* packet, 
        unsigned int len)
{
    assert(sr);
    assert(packet);

    struct ip* ip_hdr = (struct ip *)packet;
    unsigned int ip_hdr_len = (((unsigned int)(ip_hdr->ip_hl)) << 2);
    struct sr_icmp_hdr* icmp_hdr = (struct sr_icmp_hdr *)(packet + ip_hdr_len);
    struct in_addr ip_addr_temp;
    unsigned int icmp_len = len - ip_hdr_len;

    /* sr_print_icmp_hdr(icmp_hdr); */

    switch(icmp_hdr->type)
    {
        case ICMP_TYPE_ECHO:
            printf("Received ICMP echo request...\n");

            /* generate echo reply */

            memcpy(&ip_addr_temp,     &(ip_hdr->ip_src), IP_ADDR_LEN);
            memcpy(&(ip_hdr->ip_src), &(ip_hdr->ip_dst), IP_ADDR_LEN);
            memcpy(&(ip_hdr->ip_dst), &ip_addr_temp,     IP_ADDR_LEN);

            /* update IP ttl and checksum */
            ip_hdr->ip_ttl = ICMP_DEFAULT_TTL;
            ip_hdr->ip_sum = 0;
            ip_hdr->ip_sum = sr_checksum((uint16_t *)ip_hdr, ip_hdr_len >> 1,
                    CHECKSUM_DONT_PAD);

            /* fill in ICMP header */
            icmp_hdr->type = ICMP_TYPE_ECHO_REPLY;
            icmp_hdr->checksum = 0;

            /* pad if the length of the packet is odd */
            if (icmp_len & 1)
                icmp_hdr->checksum = sr_checksum((uint16_t *)icmp_hdr,
                    (icmp_len + 1) >> 1, CHECKSUM_PAD);
            else
                icmp_hdr->checksum = sr_checksum((uint16_t *)icmp_hdr,
                    (icmp_len + 1) >> 1, CHECKSUM_DONT_PAD);

            sr_send_ip_pkt_w_hdr(sr, ip_hdr, (uint8_t *)icmp_hdr, icmp_len);

            break;
        case ICMP_TYPE_TIMESTAMP:
            printf("Received ICMP timestamp request...\n");
            break;
        case ICMP_TYPE_INFO_REQUEST:
            printf("Received ICMP info request...\n");
            break;
        default:
            printf("Unknown ICMP type: %u\n", icmp_hdr->type);
    }

    return 0;
}


int sr_send_icmp_packet(struct sr_instance* sr, uint8_t type, uint8_t code,
        uint32_t ip_dst, uint32_t ip_src, uint8_t* recv_ip_buf)
{
    assert(sr);

    struct ip* ip_hdr = (struct ip *)(icmp_pkt_buf 
            + sizeof(struct sr_ethernet_hdr));
    struct sr_icmp_hdr* icmp_hdr = (struct sr_icmp_hdr *)(icmp_pkt_buf 
            + sizeof(struct sr_ethernet_hdr) + sizeof(struct ip));
    struct ip* recv_ip_hdr = (struct ip *)recv_ip_buf;
    unsigned int ip_hdr_len = 0;
    unsigned int icmp_len = 0;
    struct timespec sleep_time;
    sleep_time.tv_sec = 0;
    sleep_time.tv_nsec = 100000000L;

    /* try to lock the buffer and ARP request queue */
    pthread_mutex_lock(&icmp_buf_mutex);
    while (pthread_mutex_trylock(&arp_q_mutex))
    {
        pthread_mutex_unlock(&icmp_buf_mutex);
        /* sleep for 0.1 seconds if failed to acquire the lock */
        nanosleep(&sleep_time, 0);
        pthread_mutex_lock(&icmp_buf_mutex);
    }
    
    printf("sr_send_icmp_packet\n");

    /* assemble an IP header */
    ip_hdr->ip_hl = IP_HDR_MIN_WORDS;
    ip_hdr->ip_v = IP_V4;
    ip_hdr->ip_tos = 0;
    ip_hdr->ip_len = 0;
    ip_hdr->ip_id = 0;
    ip_hdr->ip_off = 0;

    ip_hdr->ip_ttl = ICMP_DEFAULT_TTL;
    ip_hdr->ip_p = IPPROTO_ICMP;
    ip_hdr->ip_sum = 0;
    ip_hdr->ip_src = ip_addr_to_in_addr(ip_src);
    ip_hdr->ip_dst = ip_addr_to_in_addr(ip_dst);

    icmp_hdr->type = type;

    switch(type)
    {
        case ICMP_TYPE_DST_UNREACHABLE:
        case ICMP_TYPE_TIME_EXCEEDED:
            icmp_hdr->code = code;
            icmp_hdr->unused = 0;
            break;
        default:
            fprintf(stderr, "Unknown ICMP type to send: 0x%02x\n", type);
            pthread_mutex_unlock(&arp_q_mutex);
            pthread_mutex_unlock(&icmp_buf_mutex);
            return -1;
    }

    switch(type)
    {
        case ICMP_TYPE_DST_UNREACHABLE:
        case ICMP_TYPE_TIME_EXCEEDED:

            ip_hdr_len = ((unsigned int)(recv_ip_hdr->ip_hl)) << 2;

            /* original IP header and 64 bits (8 bytes) of datagram */
            memcpy(icmp_pkt_buf 
                    + sizeof(struct sr_ethernet_hdr) 
                    + sizeof(struct ip) 
                    + sizeof(struct sr_icmp_hdr),
                    recv_ip_buf, ip_hdr_len + ICMP_IP_DATAGRAM_LEN);

            icmp_len = sizeof(struct sr_icmp_hdr) + ip_hdr_len 
                    + ICMP_IP_DATAGRAM_LEN;

            icmp_hdr->checksum = 0;

            /* pad if the length of the packet is odd */
            if (icmp_len & 1)
                icmp_hdr->checksum = sr_checksum((uint16_t *)icmp_hdr,
                    (icmp_len + 1) >> 1, CHECKSUM_PAD);
            else
                icmp_hdr->checksum = sr_checksum((uint16_t *)icmp_hdr,
                    icmp_len >> 1, CHECKSUM_DONT_PAD);

            ip_hdr->ip_len = htons(sizeof(struct ip) 
                    + sizeof(struct sr_icmp_hdr) + ip_hdr_len
                    + ICMP_IP_DATAGRAM_LEN);
            ip_hdr->ip_sum = sr_checksum((uint16_t *)ip_hdr, IP_HDR_MIN_LEN >> 1,
                    CHECKSUM_DONT_PAD);

            /* send the packet out */
            /* sr_send_ip_pkt_w_hdr(sr, ip_hdr, (uint8_t *)icmp_hdr, icmp_len); */
            sr_send_ip_to_ethernet_thread_safe(sr, icmp_pkt_buf);
            break;

        case ICMP_TYPE_ECHO:
        case ICMP_TYPE_TIMESTAMP:
        case ICMP_TYPE_INFO_REQUEST:
            break;
        default:
            break;
    }

    pthread_mutex_unlock(&arp_q_mutex);
    pthread_mutex_unlock(&icmp_buf_mutex);
    return 0;
}


void sr_print_icmp_hdr(struct sr_icmp_hdr* icmp_hdr)
{
    assert(icmp_hdr);

    uint16_t id;
    uint16_t seq;

    printf("icmp_type: 0x%02x\n", icmp_hdr->type);
    printf("icmp_code: 0x%02x\n", icmp_hdr->code);

    switch (icmp_hdr->type)
    {
        case ICMP_TYPE_ECHO:
            id = (ntohl(icmp_hdr->unused) & 0xffff0000) >> 16;
            seq = ntohl(icmp_hdr->unused) & 0x0000ffff;

            printf("icmp_id  : 0x%04x\n", id);
            printf("icmp_seq : 0x%04x\n", seq);
            break;
        default:
            printf("icmp_unused: 0x%08x\n", ntohs(icmp_hdr->unused));
    }
}

